package ants.map;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import ants.map.tile.AntTile;
import ants.map.tile.DeadAntTile;
import ants.map.tile.FoodTile;
import ants.map.tile.HillTile;
import ants.map.tile.OwnedTile;
import ants.map.tile.Tile;
import ants.map.tile.WaterTile;

public abstract class GameMap implements AntsMap {

	private static final int EXPANSION_DISTANCE_ALERT = 2;
	private static final int EXPANSION_DISTANCE_DANGER = 1;
	public static final int MY_OWNER_ID = 0;

	public static Collection<Coordinate> createDeltaRange(int radius2) {
		Collection<Coordinate> deltaCoordinates = new HashSet<Coordinate>();

		double radius = Math.sqrt(radius2);

		for (int rowDelta = 0; rowDelta <= radius; rowDelta++)
			for (int colDelta = 0; colDelta <= radius; colDelta++) {

				boolean inRange = (rowDelta * rowDelta) + (colDelta * colDelta) <= radius2;

				if (inRange) {
					if (rowDelta == 0) {
						if (colDelta == 0) {
							deltaCoordinates.add(new Coordinate(rowDelta, colDelta));
						} else {
							deltaCoordinates.add(new Coordinate(rowDelta, colDelta));
							deltaCoordinates.add(new Coordinate(rowDelta, 0 - colDelta));
						}
					} else {
						if (colDelta == 0) {
							deltaCoordinates.add(new Coordinate(rowDelta, colDelta));
							deltaCoordinates.add(new Coordinate(0 - rowDelta, colDelta));
						} else {
							deltaCoordinates.add(new Coordinate(rowDelta, colDelta));
							deltaCoordinates.add(new Coordinate(rowDelta, 0 - colDelta));
							deltaCoordinates.add(new Coordinate(0 - rowDelta, colDelta));
							deltaCoordinates.add(new Coordinate(0 - rowDelta, 0 - colDelta));
						}
					}
				}

			}

		return deltaCoordinates;
	}

	public static Collection<Coordinate> expandDeltaRange(
			Collection<Coordinate> range, int expansionDistance) {

		if (expansionDistance < 1)
			return range;

		Collection<Coordinate> newRange = new HashSet<Coordinate>(range);
		for (Coordinate c : range) {
			for (Direction direction : Direction.values()) {
				switch (direction) {
					case north:
						newRange.add(new Coordinate(c.row() - 1, c.col()));
						break;

					case south:
						newRange.add(new Coordinate(c.row() + 1, c.col()));
						break;

					case east:
						newRange.add(new Coordinate(c.row(), c.col() + 1));
						break;

					case west:
						newRange.add(new Coordinate(c.row(), c.col() - 1));
						break;
				}
			}

		}

		return expandDeltaRange(newRange, expansionDistance - 1);
	}

	private final Map<Coordinate, AntTile> antsAll = new HashMap<Coordinate, AntTile>();
	private final Set<DeadAntTile> antsDeadEnemy = new HashSet<DeadAntTile>();
	private final Set<DeadAntTile> antsDeadMine = new HashSet<DeadAntTile>();
	private final Set<AntTile> antsEnemy = new HashSet<AntTile>();
	private final Set<AntTile> antsMine = new HashSet<AntTile>();
	private final Map<Coordinate, Collection<Coordinate>> cacheAlertRange = new HashMap<Coordinate, Collection<Coordinate>>();
	private final Map<Coordinate, Collection<Coordinate>> cacheDangerRange = new HashMap<Coordinate, Collection<Coordinate>>();
	private final Map<Coordinate, Collection<Coordinate>> cacheNeighbors = new HashMap<Coordinate, Collection<Coordinate>>();
	private final Map<Coordinate, Integer> cacheThreatLevel = new HashMap<Coordinate, Integer>();
	private Set<FoodTile> food = new HashSet<FoodTile>();
	private Set<HillTile> hills = new HashSet<HillTile>();
	private final Set<HillTile> hillsEnemy = new HashSet<HillTile>();
	private final Set<HillTile> hillsMine = new HashSet<HillTile>();
	private Set<HillTile> hillsPrevious = new HashSet<HillTile>();
	private Collection<Coordinate> rangeDeltaAlert;
	private Collection<Coordinate> rangeDeltaAttack;
	private Collection<Coordinate> rangeDeltaDanger;
	private Collection<Coordinate> rangeDeltaView;
	private final Set<Coordinate> seen = new HashSet<Coordinate>();
	protected int sizeCols;
	protected int sizeRows;

	private final Set<Coordinate> visible = new HashSet<Coordinate>();

	private Set<WaterTile> water = new HashSet<WaterTile>();

	private void addAnt(AntTile tile) {
		antsAll.put(tile, tile);

		if (isPlayersTile(tile)) {

			antsMine.add((AntTile) tile);

			Collection<Coordinate> inView = getCoordinatesInViewRange(tile);
			visible.addAll(inView);
			seen.addAll(inView);

		} else
			antsEnemy.add((AntTile) tile);

	}

	@Override
	public void addAnt(int row, int col, int owner) {
		addAnt(new AntTile(row, col, owner));
	}

	private void addFood(FoodTile foodTile) {
		food.add(foodTile);
	}

	@Override
	public void addFood(int row, int col) {
		addFood(new FoodTile(row, col));
	}

	private void addHill(HillTile hillTile) {
		hills.add(hillTile);
		if (isPlayersTile(hillTile))
			hillsMine.add(hillTile);
		else
			hillsEnemy.add(hillTile);
	}

	@Override
	public void addHill(int row, int col, int owner) {
		addHill(new HillTile(row, col, owner));
	}

	@Override
	public void addWater(int row, int col) {
		addWater(new WaterTile(row, col));
	}

	private void addWater(WaterTile waterTile) {
		water.add(waterTile);
	}

	private Coordinate applyDeltaCoordinate(Coordinate c,
			Coordinate deltaCoordinate) {
		int newRow = normalizeRow(c.row() + deltaCoordinate.row());
		int newCol = normalizeCol(c.col() + deltaCoordinate.col());
		return new Coordinate(newRow, newCol);
	}

	private Collection<Coordinate> applyDeltaCoordinates(Coordinate c,
			Collection<Coordinate> deltaCoordinates) {
		Collection<Coordinate> appliedRange = new HashSet<Coordinate>();

		for (Coordinate deltaCoordinate : deltaCoordinates)
			appliedRange.add(applyDeltaCoordinate(c, deltaCoordinate));

		return appliedRange;
	}

	public Collection<Coordinate> getAlertZone(Coordinate c) {
		if (cacheAlertRange.containsKey(c))
			return cacheAlertRange.get(c);

		Collection<Coordinate> alertRange = applyDeltaCoordinates(c,
				rangeDeltaAlert);
		cacheAlertRange.put(c, alertRange);

		return alertRange;
	}

	protected Collection<AntTile> getAllAnts() {
		return antsAll.values();
	}

	public AntTile getAnt(Coordinate c) {
		return antsAll.get(c);
	}

	public int getAntThreatLevel(AntTile ant) {
		if (cacheThreatLevel.containsKey(ant))
			return cacheThreatLevel.get(ant);

		int countEnemyAnt = 0;

		for (Coordinate rangeCoordinate : getDangerZone(ant)) {
			if (isAnt(rangeCoordinate)) {
				AntTile nearbyAnt = getAnt(rangeCoordinate);
				if (ant.getOwner() != nearbyAnt.getOwner())
					countEnemyAnt++;
			}
		}

		cacheThreatLevel.put(ant, countEnemyAnt);

		return countEnemyAnt;
	}

	private Collection<Coordinate> getCoordinatesInViewRange(Coordinate center) {
		Set<Coordinate> inRange = new HashSet<Coordinate>();
		for (Coordinate deltaCoordinate : rangeDeltaView)
			inRange.add(applyDeltaCoordinate(center, deltaCoordinate));
		return inRange;
	}

	public Collection<Coordinate> getDangerZone(Coordinate c) {
		if (cacheDangerRange.containsKey(c))
			return cacheDangerRange.get(c);

		Collection<Coordinate> dangerRange = applyDeltaCoordinates(c,
				rangeDeltaDanger);
		cacheDangerRange.put(c, dangerRange);

		return dangerRange;
	}

	public Direction getDirection(Coordinate from, Coordinate to) {
		int deltaRow = getRealDelta(sizeRows, from.row(), to.row());
		int deltaCol = getRealDelta(sizeCols, from.col(), to.col());

		if (Math.abs(deltaRow) >= Math.abs(deltaCol)) {
			if (deltaRow > 0)
				return Direction.south;
			if (deltaRow < 0)
				return Direction.north;
		}

		if (deltaCol > 0)
			return Direction.east;
		else if (deltaCol < 0)
			return Direction.west;

		return null;
	}

	public int getDistanceManhattan(Coordinate from, Coordinate to) {
		int deltaRow = getRealDelta(sizeRows, from.row(), to.row());
		int deltaCol = getRealDelta(sizeCols, from.col(), to.col());

		return Math.abs(deltaRow) + Math.abs(deltaCol);
	}

	public Collection<AntTile> getEnemyAnts() {
		return antsEnemy;
	}

	public Collection<AntTile> getEnemyAntsInDangerZone(Coordinate c) {
		Collection<AntTile> enemyAnts = new HashSet<AntTile>();

		Collection<Coordinate> dangerRange = getDangerZone(c);
		for (Coordinate rangeCoordinate : dangerRange) {
			if (isEnemyAnt(rangeCoordinate))
				enemyAnts.add(getAnt(rangeCoordinate));
		}

		return enemyAnts;
	}

	public Collection<AntTile> getMyAnts() {
		return antsMine;
	}

	public Collection<HillTile> getMyHills() {
		return hillsMine;
	}

	public Collection<Coordinate> getNeighbors(Coordinate c) {
		if (!cacheNeighbors.containsKey(c)) {
			Collection<Coordinate> neighbors = new ArrayList<Coordinate>(4);
			for (Direction direction : Direction.values())
				neighbors.add(getNewCoordinate(c, direction));

			cacheNeighbors.put(c, neighbors);
		}

		return new ArrayList<Coordinate>(cacheNeighbors.get(c));
	}

	private Coordinate getNewCoordinate(Coordinate c, Direction direction) {
		switch (direction) {
			case north:
				return new Coordinate(normalizeRow(c.row() - 1), c.col());
			case south:
				return new Coordinate(normalizeRow(c.row() + 1), c.col());
			case east:
				return new Coordinate(c.row(), normalizeCol(c.col() + 1));
			case west:
				return new Coordinate(c.row(), normalizeCol(c.col() - 1));
		}
		return c;
	}

	private int getRealDelta(int dimension, int p1, int p2) {
		if (p1 == p2)
			return 0;

		int bestDelta = Integer.MAX_VALUE;
		for (int delta : Arrays.asList((p2 - dimension) - p1, p2 - p1,
				(p2 + dimension) - p1)) {
			if (Math.abs(delta) < Math.abs(bestDelta))
				bestDelta = delta;
		}

		return bestDelta;
	}

	@Override
	public void initialize(int rows, int cols, int viewradius2, int attackradius2) {
		this.sizeRows = rows;
		this.sizeCols = cols;
		this.rangeDeltaView = createDeltaRange(viewradius2);
		this.rangeDeltaAttack = createDeltaRange(attackradius2);
		this.rangeDeltaDanger = expandDeltaRange(rangeDeltaAttack,
				EXPANSION_DISTANCE_DANGER);
		this.rangeDeltaAlert = expandDeltaRange(rangeDeltaAttack,
				EXPANSION_DISTANCE_ALERT);
	}

	public boolean isAnt(Coordinate c) {
		return antsAll.containsKey(c);
	}

	public boolean isEnemyAnt(Coordinate c) {
		return antsEnemy.contains(c);
	}

	protected boolean isEnemyDeadAnt(Coordinate c) {
		return antsDeadEnemy.contains(c);
	}

	public boolean isEnemyInRange(Coordinate c) {
		Collection<Coordinate> dangerRange = getDangerZone(c);

		for (Coordinate rangeCoordinate : dangerRange)
			if (isEnemyAnt(rangeCoordinate))
				return true;

		return false;
	}

	protected boolean isFood(Coordinate c) {
		return food.contains(c);
	}

	protected boolean isHillEnemy(Coordinate c) {
		return hillsEnemy.contains(c);
	}

	public boolean isMyAnt(Coordinate c) {
		return antsMine.contains(c);
	}

	protected boolean isMyDeadAnt(Coordinate c) {
		return antsDeadMine.contains(c);
	}

	private boolean isPlayersTile(Tile tile) {
		return tile instanceof OwnedTile
				&& MY_OWNER_ID == ((OwnedTile) tile).getOwner();
	}

	protected boolean isSeen(Coordinate c) {
		return seen.contains(c);
	}

	protected boolean isVisible(Coordinate c) {
		return visible.contains(c);
	}

	public boolean isWater(Coordinate c) {
		return water.contains(c);
	}

	private int normalizeCol(int col) {
		if (col < 0)
			return col + sizeCols;
		else if (col >= sizeCols)
			return col - sizeCols;
		return col;
	}

	private int normalizeRow(int row) {
		if (row < 0)
			return row + sizeRows;
		else if (row >= sizeRows)
			return row - sizeRows;
		return row;
	}

	private void removeAnt(DeadAntTile deadAntTile) {
		if (isPlayersTile(deadAntTile))
			antsDeadMine.add(deadAntTile);
		else
			antsDeadEnemy.add(deadAntTile);
	}

	@Override
	public void removeAnt(int row, int col, int owner) {
		removeAnt(new DeadAntTile(row, col, owner));
	}

	@Override
	public void reset() {
		antsAll.clear();
		antsMine.clear();
		antsEnemy.clear();

		antsDeadMine.clear();
		antsDeadEnemy.clear();

		food.clear();

		hillsPrevious = hills;
		hills = new HashSet<HillTile>();
		hillsEnemy.clear();
		hillsMine.clear();

		visible.clear();

		// dangerRangeCache.clear();
		cacheThreatLevel.clear();
	}

	@Override
	public void update() {
		updateHills();
	}

	private void updateHills() {
		for (HillTile previousHill : hillsPrevious) {
			if (!isVisible(previousHill))
				addHill(previousHill);
		}
	}

}
